package com.newmainsoftech.ant.script;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
public class AntrunScriptUtilTemplateAspect {
	// For logging --------------------------------------------------------------------------------
	private Logger logger = LoggerFactory.getLogger( this.getClass());
		protected Logger getLogger() {
			return logger;
		}
		protected void setLogger(Logger logger) {
			this.logger = logger;
		}
	private static ThreadLocal<Boolean> isLogSentToLoggerThreadLocal = new ThreadLocal<Boolean>();
		protected static ThreadLocal<Boolean> getIsLogSentToLoggerThreadLocal() {
			return isLogSentToLoggerThreadLocal;
		}
	// --------------------------------------------------------------------------------------------
		
	// For inheritance of owner among objects of classes implemented AntOwnerAccessorContract -----
	static private ThreadLocal<AntOwnerAccessorImpl> initialOwnerThreadLocal 
	= new ThreadLocal<AntOwnerAccessorImpl>();
		static ThreadLocal<AntOwnerAccessorImpl> getInitialOwnerThreadLocal() {
			return initialOwnerThreadLocal;
		}
		
		Object getOwnerOutOfThreadLocal() {
			AntOwnerAccessorContract antOwnerAccessor 
			= AntrunScriptUtilTemplateAspect.getInitialOwnerThreadLocal().get();
			if ( antOwnerAccessor == null) return null;
			
			Object owner = antOwnerAccessor.getOwner();
			return owner;
		}
		
	@Pointcut( 
			"call( * com.newmainsoftech.ant.script.AntOwnerAccessorContract+.*(..)) " 
			+ "&& this( com.newmainsoftech.ant.script.AntOwnerAccessorContract+)" 
			)
	public static void pointcutOnCallOnAntOwnerAccessorAdapteeMethods() { 
	}
	@Pointcut( 
			value="cflowbelow( pointcutOnCallOnAntOwnerAccessorAdapteeMethods()) " 
			)
	public static void pointcutOnSubSequentialCallOnAntOwnerAccessorAdapteeMethods() {}
	
	@Pointcut( 
			value="pointcutOnCallOnAntOwnerAccessorAdapteeMethods() " 
					+ "&& !pointcutOnSubSequentialCallOnAntOwnerAccessorAdapteeMethods() " 
					+ "&& this( com.newmainsoftech.ant.script.AntOwnerAccessorContract+) " 
					+ "&& !this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplateAspect) " 
					+ "&& !within( com.newmainsoftech.ant.script.AntrunScriptUtilTemplateAspect) " 
					+ "&& !cflow( adviceexecution()) "  
					+ "&& if()" 
			)
	public static boolean pointcutOnTopMostCallOnAntOwnerAccessorAdapteeMethods( 
			JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart) 
	{
		Object thisObj = joinPoint.getThis();
		Object targetObj = joinPoint.getTarget();
		if ( thisObj != targetObj) { // Only when thisObj != targetObj, inherit owner of thisObj to targetObj 
			return true;
		}
		return false;	
	}
	
	Project getProject( Object owner) {
		if ( owner instanceof Project) {
			return (Project)owner;
		}
		else {
			try {
				Method getProjectMethod = owner.getClass().getMethod( "getProject", new Class[]{});
				getProjectMethod.setAccessible( true);
				return (Project)(getProjectMethod.invoke( owner, new Object[]{}));
			}
			catch( Exception exception) { 
				return null;
			}
		}
	}
	
	
	
	public static final String LogToOwnerPropertyName = "ant-util.log-to-owner";
	
		@Before( 
				value="pointcutOnTopMostCallOnAntOwnerAccessorAdapteeMethods( joinPoint, enclosingStaticPart)"
				)
		public void beforeAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods( 
				JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart) 
		{
			StringBuffer logMsgsStringBuffer = new StringBuffer();
				logMsgsStringBuffer.append( 
						String.format(
								"Entered beforeAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods advise " 
								+ "as the entry point of the join-point of %1$s as the most-outer call in " 
								+ "a sequence of operations by %2$s sub-class(es).",
								joinPoint.getSignature().toShortString(), 
								AntrunScriptUtilTemplate.class.getName().substring( 
										AntrunScriptUtilTemplate.class.getName().lastIndexOf( ".") + 1)
								)
						);
			
			AntOwnerAccessorImpl antOwnerAccessorImpl = new AntOwnerAccessorImpl();
			Object owner = ((AntOwnerAccessorContract)(joinPoint.getThis())).getOwner();
			antOwnerAccessorImpl.setOwner( owner);
			
			boolean isLogSentToOwner = false;
				Project project = getProject( owner);
				if ( project != null) {
					String isLogSentToOwnerStr 
					= project.getProperty( AntrunScriptUtilTemplateAspect.LogToOwnerPropertyName);
					
					if ( Boolean.TRUE.equals( Boolean.valueOf( isLogSentToOwnerStr.trim()))) {
						isLogSentToOwner = true;
					}
				}
			AntrunScriptUtilTemplateAspect.getIsLogSentToLoggerThreadLocal()
			.set( Boolean.valueOf( isLogSentToOwner));
			
			
			
	System.out.println(
		String.format(
				"Having set %1$s as owner to ThreadLocal",
				( owner == null) ? "null" : owner.toString()
				)
		);
			AntrunScriptUtilTemplateAspect.getInitialOwnerThreadLocal().set( antOwnerAccessorImpl);
	System.out.println( "Exited beforeAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods");
		}
		
		@After( 
				value="pointcutOnTopMostCallOnAntOwnerAccessorAdapteeMethods( joinPoint, enclosingStaticPart)"
				)
		public void afterAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods( 
				JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart) 
		{
			
			StringBuffer logMsgsStringBuffer 
			= AntrunScriptUtilTemplateAspect.getLogMessagesThreadLocal().get();
				logMsgsStringBuffer.append( 
						String.format(
								"Entered afterAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods advise " 
								+ "as the exit point of the join-point of %1$s as the most-outer call in " 
								+ "a sequence of operations by %2$s sub-class(es).",
								joinPoint.getSignature().toShortString(), 
								AntrunScriptUtilTemplate.class.getName().substring( 
										AntrunScriptUtilTemplate.class.getName().lastIndexOf( ".") + 1)
								)
						);
				
			// Get owner and check whether property of ant-util.log-to-owner is true (default to false) 
			Object owner = AntrunScriptUtilTemplateAspect.getInitialOwnerThreadLocal().get();
			if ( )
			
			AntrunScriptUtilTemplateAspect.getInitialOwnerThreadLocal().remove();
	/* System.out.println( "Exited afterAdvisedTopMostCallOnAntOwnerAccessorAdapteeMethods");
	*/
			AntrunScriptUtilTemplateAspect.getLogMessagesThreadLocal().remove();
		}
	
	@Pointcut( 
			value="pointcutOnSubSequentialCallOnAntOwnerAccessorAdapteeMethods() " 
					+ "&& call( * com.newmainsoftech.ant.script.AntOwnerAccessorContract+.getOwner(..)) " 
					+ "&& this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplate+) " 
					+ "&& !this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplateAspect) " 
					+ "&& !within( com.newmainsoftech.ant.script.AntrunScriptUtilTemplateAspect) " 
					+ "&& if()"
			)
	public static boolean pointcutAtCallOnGetOwnerMethod( 
			JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart)
	{
		if ( joinPoint.getThis() == joinPoint.getTarget()) {
			MethodSignature methodSignature = (MethodSignature)(joinPoint.getSignature());
			MethodSignature enclosingMethodSignature 
			= (MethodSignature)(enclosingStaticPart.getSignature());
			if ( methodSignature.equals( enclosingMethodSignature)) { // Prevent from falling into infinite loop 
				return false;
			}
		}
		return true;
	}
	
		@Around( 
				value="pointcutAtCallOnGetOwnerMethod( joinPoint, enclosingStaticPart)" 
				)
		public Object aroundAdvisedGetOwnerMethod( 
				JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart, 
				ProceedingJoinPoint proceedingJoinPoint) 
		throws Throwable {
			StringBuffer logMsgsStringBuffer 
			= AntrunScriptUtilTemplateAspect.getLogMessagesThreadLocal().get();
				MethodSignature methodSignature = (MethodSignature)(proceedingJoinPoint.getSignature());
				logMsgsStringBuffer.append( 
						String.format(
								"Entered aroundAdvisedGetOwnerMethod advise for the join-point of " 
								+ "%1$s method being called from %2$s",
								methodSignature.getMethod().getName(), 
								enclosingStaticPart.getSignature().toShortString()
								)
						);
				
			Object owner = getOwnerOutOfThreadLocal();
			if ( owner != null) {
				logMsgsStringBuffer.append( 
						String.format(
								"Exited aroundAdvisedGetOwnerMethod advise for the join-point of " 
								+ "%1$s method being called from %2$s by returning %3$s as owner stocked " 
								+ "at begining of sequence of operations by %4$s sub-class(es).",
								methodSignature.getMethod().getName(), 
								enclosingStaticPart.getSignature().toShortString(),
								owner.toString(),
								AntrunScriptUtilTemplate.class.getName().substring( 
										AntrunScriptUtilTemplate.class.getName().lastIndexOf( ".") + 1)
								)
						);
				return owner;
			}
			
			try {
				owner = proceedingJoinPoint.proceed( proceedingJoinPoint.getArgs());
					logMsgsStringBuffer.append( 
							String.format(
									"Exited aroundAdvisedGetOwnerMethod advise for the join-point of " 
									+ "%1$s method being called from %2$s by returning %3$s as owner " 
									+ "obtained by delegating to actual %1$s method of %4$s", 
									methodSignature.getMethod().getName(), 
									enclosingStaticPart.getSignature().toShortString(),
									owner.toString(),
									proceedingJoinPoint.getTarget().toString()
									)
							);
				return owner;
			}
			catch( Throwable throwable) {
				if ( throwable instanceof RuntimeException) {
					throw (RuntimeException)throwable;
				}
				Class<?>[] exceptionTypeArray = methodSignature.getExceptionTypes();
				for( Class<?> exceptionType : exceptionTypeArray) {
					if ( !throwable.getClass().isAssignableFrom( exceptionType)) {
						try {
							throwable.getClass().asSubclass( exceptionType);
						}
						catch( Throwable tempThrowable) {
							throw throwable;
						}
					}
				}
				throw new RuntimeException( throwable);
			}
		}
	
	
	@Pointcut( 
			"call( com.newmainsoftech.ant.script.AntOwnerAccessorContract+.new(..)) " 
			+ "&& this( com.newmainsoftech.ant.script.AntOwnerAccessorContract+) " 
			+ "&& !this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplate) " 
			+ "&& !this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplateAspect)") 
	public static void pointcutOnCallOfAntOwnerAccessorConstructors() {}
		@AfterReturning( pointcut="pointcutOnCallOfAntOwnerAccessorConstructors()")
		public void afterAdviseAtCallOfAntOwnerAccessorConstructors( JoinPoint joinPoint) {
System.out.println( "Entered afterAdviseAtCallOfAntOwnerAccessorConstructors");
			if ( joinPoint.getThis() == joinPoint.getTarget()) {
				throw new IllegalMonitorStateException( 
						"Unexpected state: this and target in advise context for call pointcut " 
						+ "(=pointcutOnCallOfAntOwnerAccessorConstructors()) are identical.");
			}
			AntOwnerAccessorContract creator = (AntOwnerAccessorContract)(joinPoint.getThis());
			AntOwnerAccessorContract createe = (AntOwnerAccessorContract)(joinPoint.getTarget());
			Object owner = creator.getOwner();
			createe.setOwner( owner);
System.out.println( 
		String.format( 
				"Having set %1$s as owner to " + createe.toString(), 
				((owner == null) ? "null" : owner.toString())
				)
		);

System.out.println( "Exited afterAdviseAtCallOfAntOwnerAccessorConstructors");
		}
	// --------------------------------------------------------------------------------------------
	
	// For logging about entry and exit of each public method of 
	// AntrunScriptUtilTemplate sub classes -------------------------------------------------------
/*
	@Pointcut( "execution( public * com.newmainsoftech.ant.script.AntrunScriptUtilTemplate+.*(..))")
	public static void pointcutOnExecutionOfPublicMethods() {}
	@Pointcut( 
			"execution( public * com.newmainsoftech.ant.script.AntLogMethodsContract.*(..))" 
			+ "&& this( com.newmainsoftech.ant.script.AntrunScriptUtilTemplate+)")
	public static void pointcutOnExecutionOfLogMethods() {}
	
		@Before( value="pointcutOnExecutionOfPublicMethods() && !pointcutOnExecutionOfLogMethods()")
		public void beforeAdviseForLoggingExecutionOfPublicMethods( JoinPoint joinPoint) {
System.out.println( "Entered beforeAdviseForLoggingExecutionOfPublicMethods");
			MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
			String methodName = methodSignature.getMethod().getName();
			StringBuilder argumentsStringBuilder = new StringBuilder();
			Object[] argArray = joinPoint.getArgs();
				if ( argArray.length > 0) {
					String[] argNameArray = methodSignature.getParameterNames();
					Class<?>[] argTypeArray = methodSignature.getParameterTypes();
					for( int argIndex = 0; argIndex < argArray.length; argIndex++) {
						String argTypeName = argTypeArray[ argIndex].getSimpleName();
							int typeNameIndex = argTypeName.lastIndexOf( ".");
							if ( typeNameIndex > -1) {
								argTypeName = argTypeName.substring( typeNameIndex + 1);
							}
						String argName = argNameArray[ argIndex];
						String argValue;
							if ( argArray[ argIndex] == null) {
								argValue = "null";
							}
							else if ( argArray[ argIndex].getClass().isArray()) {
								argValue = Arrays.toString( (Object[])argArray[ argIndex]);
							}
							else {
								argValue = argArray[ argIndex].toString();
							}
						argumentsStringBuilder.append(
								String.format( "%1$s %2$s %3$s", argTypeName, argName, argValue));
					} // for
				}
				
			String enterInfo 
			= String.format(
					"Entered %1$s method" + (( argArray.length < 1) ? "" : " with arguments of %2$s"), 
					methodName, 
					argumentsStringBuilder.toString());
			((AntrunScriptUtilTemplate)joinPoint.getTarget()).log( enterInfo, LogLevel.VERBOSE);
System.out.println( "Exited beforeAdviseForLoggingExecutionOfPublicMethods");
		}
		
		@AfterReturning( 
				pointcut="pointcutOnExecutionOfPublicMethods() && !pointcutOnExecutionOfLogMethods()", 
				returning="returnedObj")
		public void afterAdviseForLoggingExecutionOfPublicMethods( JoinPoint joinPoint, Object returnedObj) {
System.out.println( "Entered afterAdviseForLoggingExecutionOfPublicMethods");
			MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
			String methodName = methodSignature.getMethod().getName();
			
			String returnedObjStr = "";
				if ( !methodSignature.getReturnType().equals( Void.class)) {
					if ( returnedObj == null) {
						returnedObjStr = "null";
					}
					else if ( returnedObj.getClass().isArray()) {
						returnedObjStr = Arrays.toString( (Object[])returnedObj);
					}
					else {
						returnedObjStr = returnedObj.toString();
					}
				}
			
			String exitInfo 
			= String.format( 
					"Exited %1$s method" + ((returnedObjStr.length() > 0) ? (" " + returnedObjStr) : ""), 
					methodName, 
					returnedObjStr);
			((AntrunScriptUtilTemplate)joinPoint.getTarget()).log( exitInfo, LogLevel.VERBOSE);
System.out.println( "Exited afterAdviseForLoggingExecutionOfPublicMethods");
		}
*/
	// --------------------------------------------------------------------------------------------
}
